Skip to content

Fix TSP None type encoding#4043

Open
WilliamK112 wants to merge 1 commit into
facebook:mainfrom
WilliamK112:codex/tsp-none-type-4035
Open

Fix TSP None type encoding#4043
WilliamK112 wants to merge 1 commit into
facebook:mainfrom
WilliamK112:codex/tsp-none-type-4035

Conversation

@WilliamK112

Copy link
Copy Markdown

Summary

Fixes #4035.

This changes Pyrefly's TSP conversion for None from an off-spec BuiltInType named none to a ClassType instance declared as types.NoneType. The protocol's BuiltInType.name contract only allows sentinel names such as unknown, any, ellipsis, never, and noreturn, so encoding None as a class keeps str | None and other unions reconstructable by TSP consumers.

The change also updates the TSP interaction coverage for x = None so the actual wire response is checked as a class declaration rather than a builtin sentinel.

Test Plan

  • cargo +stable-x86_64-pc-windows-gnu test -p pyrefly tsp::type_conversion::tests:: -- --nocapture
  • cargo +stable-x86_64-pc-windows-gnu test -p pyrefly test_get_computed_type_none_is_class -- --nocapture
  • cargo +stable-x86_64-pc-windows-gnu test -p pyrefly tsp::type_conversion::tests::test_function_declaration_resolves_special_function_via_export -- --exact --nocapture
  • RUSTUP_TOOLCHAIN=stable-x86_64-pc-windows-gnu python test.py --mode cargo --no-test --no-tensor-shapes --no-conformance --no-jsonschema
  • git diff --check

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

@meta-codesync

meta-codesync Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@kinto0 has imported this pull request. If you are a Meta employee, you can view this in D110961669.

@rchiodo rchiodo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this — the core change (stop emitting the off-spec none builtin and encode None as a NoneType ClassType instead) is the right fix for #4035.

My main concern is that the NoneType location is re-derived by name and hardcoded to the types module, which diverges from pyrefly's authoritative, version-aware stdlib.none_type() on Python < 3.10. I'd suggest routing None through the real stdlib.none_type() class so the encoding is version-correct and identical to an explicit types.NoneType annotation. Details inline. It would also be worth an end-to-end Pylance check to confirm the hover symptom actually round-trips.

PyreflyType::Any(_) => builtin("any"),
PyreflyType::Never(_) => builtin("never"),
PyreflyType::None => builtin("none"),
PyreflyType::None => self.types_class("NoneType", TypeFlags::INSTANCE),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior change here is correct — builtin("none") was off-spec. My design concern is how the target is derived: types_class re-derives the NoneType location by name (types + "NoneType") instead of using pyrefly's authoritative stdlib.none_type(), which already resolves the version-correct location.

Could we instead convert the actual stdlib.none_type() ClassType through the existing convert_class_type(..., TypeFlags::INSTANCE) path? That makes None encode identically to an explicit types.NoneType annotation and inherits the correct module/range automatically, so there's no duplicated module/name/path logic. TypeConverter doesn't hold the stdlib today, but the server.rs caller does (it computes get_stdlib) and could pass the resolved class (or its qname) down. See find_definition_for_none in state/lsp.rs for the existing precedent that follows stdlib.none_type().

let symbol = Name::new(name);
if let Some((module_path, lsp_range)) = self
.resolve_export
.and_then(|resolve| resolve(ModuleName::types(), &symbol))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concrete version-correctness issue with hardcoding ModuleName::types(): stdlib.none_type() resolves NoneType from types only on Python 3.10+, and from _typeshed on older versions (see none_location in crates/pyrefly_types/src/stdlib.rs). On a project targeting < 3.10 this lookup points at a different module than pyrefly's own None resolution / goto-definition, so the TSP declaration and the in-editor "go to definition" for None would disagree. Deriving the location from stdlib.none_type()'s qname avoids the divergence.

/// Build a declaration for a class in `types.pyi`.
fn make_types_class_declaration(name: &str) -> RegularDeclaration {
let module_path =
pyrefly_python::module_path::ModulePath::bundled_typeshed(PathBuf::from("types.pyi"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, same caveat as the resolver path: this fallback hardcodes types.pyi, which is only correct for 3.10+. It only fires when no export resolver is available (unit tests), so impact is low — but if the location is derived from stdlib.none_type() this hardcoded file (and the whole helper) goes away.

assert_eq!(name, Some("none"), "Expected builtin name 'none'");
let declaration = result.get("declaration").expect("Expected declaration");
let name = declaration.get("name").and_then(|v| v.as_str());
assert_eq!(name, Some("NoneType"), "Expected class name 'NoneType'");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (and the unit tests) verify the wire shape — Class + name NoneType — but not the actual symptom from #4035: that Pylance now renders str | None instead of str | Unknown. The issue notes the consumer models this as builtins.NoneType; since we're emitting types.NoneType, it'd be reassuring to confirm end-to-end against a real Pylance client that the hover round-trips (i.e. that Pylance keys off the class name rather than the builtins module).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TSP: None encoded as BuiltInType name:"none", violating the documented BuiltInType.name contract

3 participants